/*
 * jQuery Plugin: Haven Autocomplete Input
 * Version 1.0 2010-11-29
 *
 * Copyright (c) 2010 Herry Liao/Steven Yang
 * Licensed jointly under the GPL and MIT licenses,
 * choose which one suits your project best!
 * 
 */
(function($, undefined) {
  
  var emptyFunc = function(){};
  var KEY = {
    BACKSPACE : 8,
    TAB : 9,
    RETURN : 13,
    LEFT : 37,
    UP : 38,
    RIGHT : 39,
    DOWN : 40,
    DEL : 46
  };
  
  var debug = function(msg) {
    if(window.console && window.console.log) {
      window.console.log(msg);
    }
  };
  
  var parseJson = function(data) {
    
    if($.isArray(data)) return data;
    if(typeof data == 'string') {
      try {
        return $.parseJson(data)
      }
      catch(e) {}
    }
    else if(!$.isFunction(data)) return data;
  };

  $.havenInput = {
    focusLi : function($li) {
      $li.find("a").focus();
      $li.find("input").focus();
    },
    deleteItem : function($delLi, $toFocusLi, $origInput, $ul) {
      if ($toFocusLi && $toFocusLi.length) 
        this.focusLi($toFocusLi);
      else 
        this.focusLi($delLi.siblings("li"))
      
      $delLi.remove();
      this.updateValue($origInput, $ul);
    },
    updateValue : function($origInput, $ul) {
      var valueArr = [];
      $ul.find("li[haveninputid]").each(function() {
        valueArr.push($(this).attr("haveninputid"));
      });
      $origInput.val(valueArr);
    },
    focusPrevItem : function($li) {
      this.focusLi($li.prev("li"));
    },
    focusNextItem : function($li) {
      $li = $li.next("li");
      this.focusLi($li);
      $li.find(":text").focus();
    },
    _deleteItem : function(_$origInput, _$ul, _$input) {
      return function(id) {
        if (id) {
          id = $.makeArray(id);
          $.each(id, function() {
            _$ul.find("li[haveninputid='" + this + "']").remove();
          });
        }
        else {//if id not passed then delete all
          _$ul.find("li[haveninputid]").remove();
        }
        _$input.val("");
        $.havenInput.updateValue(_$origInput, _$ul);
      };
    },
    _addItem : function(_$origInput, _$ul, _$inputLi, _maxItem, _retrieveId, _retrieveValue, _highlightExist){
      return function(vid, vname) { 
        var add = function(id, name){
        
          var $li = _$ul.find("li[haveninputid=" + id + "]");
          if ($li.length === 0 && (_maxItem == 0 || _$ul.find("li[haveninputid]").length < _maxItem)) {
            $li = $("<li class='havenInput-block' haveninputid='" + id + "'></li>");
            var $p = $("<p class='havenInput-p'></p>").html(name);
            var $closeBtn = $("<a href='#' class='havenInput-delete'>x</a>");
            
            $li.append($p).append($closeBtn);
            _$inputLi.before($li);
            
            $.havenInput.updateValue(_$origInput, _$ul);
          }
          else {
            if (_highlightExist) {
              $li.addClass("havenInput-highlight");
              setTimeout(function(){
                $li.removeClass("havenInput-highlight");
              }, 500);
            }
          }
        };
        if (arguments.length == 1) {
          if($.isArray(vid)) {
            $.each(vid, function(i, v){
              if ($.isArray(v) && v.length === 2) {
                add(v[0], v[1]);
              }
              else if (typeof v === 'string') {
                add(v, v);
              }
              else {
                add(_retrieveId(v), _retrieveValue(v));
              }
            });
          }
          else if (typeof vid == 'string') {
            add(vid, vid);
          }
          else if (typeof vid == 'object') {
            add(_retrieveId(vid), _retrieveValue(vid));
          }
        }
        else {
          add(vid, vname);
        }
      };
    },
    _value : function(_$origInput) {
      return function(value) {
        if(!value || typeof value === 'string') {
          return $.havenInput._getValue(_$origInput, value);
        }
        else if ($.isArray(value) || !$.isFunction(value)){
          try {
            _$origInput.havenInput("deleteItem")
            return _$origInput.havenInput("addItem", value);
          }
          catch(e) {
            debug("data cannot be added "+e.name+" : "+ e.message)
          }
        }
      };
    },
    _wrapAutocomplete : function($target, addItem, params) {
      if ($target.length == 0 || !params.source) {
        return;
      }
      
      var source = (function(){
        var s = params.source;
        if (typeof s === 'string') {
          var d = parseJson(s);
          if(d) return d;
          else {
            var cache = {};
            return function(req, res){
              if (p.cache && cache[req.term]) {
                res(cache[req.term])
              }
              else {
                var data = {};
                data[p.acParamName] = req.term
                data = $.extend(data, p.data);
                $.getJSON(s, data, function(results){
                  p.onDataReceive(results);
                  if(p.cache) cache[req.term] = results
                  res(results);
                });
              }
            };
          }
        }
        else {
          return s;
        }
      })();
      var p = $.extend({}, params, {
        "source" : source,
        focus : function(e, ui) {
          p.onAcFocus.call($target, e, ui);
          return false;
        },
        select : function(e, ui) {
          p.onAcSelect.call($target, e, ui);
          $target.focus();
          addItem(ui.item);
          return false;
        },
        change : function(e, ui) {
          p.onAcChange.call($target, e, ui);
          return false;
        }
      });
      
      $target.autocomplete(p)
      .data( "autocomplete" )._renderItem = function( ul, item ) {
        return $( "<li></li>" )
        .data( "item.autocomplete", item )
        .append( "<a>" + p.itemFormat(ul, item) + "</a>" )
        .appendTo( ul );
      };
      
      $target.change(function() {
        
        var item = $target.data("autocomplete").selectedItem;
        if (item != null) {
          if (value !== null) {
            $target.val(p.retrieveValue(item));
          }
        } else {
          $target.val("");
          p.doClean.call($target);
        }
        return false;
      });
      
      // fix firefox for having proble with Chinese input event
      $target.bind( "text.autocomplete", function(d) {
        $target.trigger("keydown.autocomplete");
      });
    },
    _defaultRetrieveFunc : function(item) {return item.value},
    _getValue : function($this, format) {
      var val = $this.val();
      var name = $this.attr("name");
      switch(format) {
        case "multiPair" : 
          return name +"="+val.split(",").join("&"+name+"=")
        case "namePair" :
          return name +"=" + val
        case "array" :
          return val.split(",")
        default :
          return val
      }
    },
    _havenInput : function($this, opts) {
  
      // build element specific options
      var o = $.meta ? $.extend({}, opts, $this.data()) : opts;
            
      var $ul = $("<ul class='havenInput-havenInput'></ul>");
      var $inputLi = $("<li></li>");
      var $input = $("<input type='text'/>");
      
      $this.val("").hide().after($ul);
      $ul.append($inputLi.append($input));
          
      var addItem = $.havenInput._addItem($this, $ul, $inputLi, o.maxItem, o.retrieveId, o.retrieveValue, o.highlightExist);
      $this.data("havenInput.addItem", addItem);
      $this.data("havenInput.deleteItem", $.havenInput._deleteItem($this, $ul, $input));
      $this.data("havenInput.value", $.havenInput._value($this));
      
      $ul
        .click(function(e) {
          e.stopPropagation();
          $input.focus();
        })
        .delegate(".havenInput-block", "click", function(e) {
          e.stopPropagation();
          $(this).find(".havenInput-delete").focus();
        })
        .delegate(".havenInput-delete", "click", function() {
          var $li = $(this).closest("li");
          $.havenInput.deleteItem($li, $li.next("li"), $this, $ul);
        })
        .delegate(".havenInput-delete", "keydown", function(e){
          var fn = $.havenInput["keyEvent"+e.keyCode];
          var $li = $(this).closest("li");
          if (fn && $.isFunction(fn)) fn($li, $this, $ul);
          return false;
        })
        .delegate(".havenInput-delete", "focus", function() {
            $ul.find("li").removeClass("havenInput-focus");
            $(this).closest("li").addClass("havenInput-focus");
        });
      
      $input.keydown(function(e) {
  
        switch (e.keyCode) {
          case KEY.BACKSPACE :
          case KEY.LEFT :
            if ($input.val().length == 0) {
              $.havenInput.focusPrevItem($inputLi);
              return false;
            }
            break;
        }
      })
      .focus(function() {
        $ul.find("li").removeClass("havenInput-focus");
      });
  
      if (o.useAC && $.fn.autocomplete) {
        $.havenInput._wrapAutocomplete($input, addItem, o);
      }
	  else {
	  	$input.keydown(function(e){
			if(e.keyCode == KEY.RETURN) {
				addItem($input.val(), $input.val());
				$input.val("");
			}
		});
	  }
      if(o.initData) {
        addItem(o.initData);
      }
    }
  };
  
  $.havenInput["keyEvent"+KEY.BACKSPACE] = function($li, $origInput, $ul) {
    $.havenInput.deleteItem($li, $li.prev("li"), $origInput, $ul);
  };
  
  $.havenInput["keyEvent" + KEY.DEL] = function($li, $origInput, $ul){
    $.havenInput.deleteItem($li, $li.next("li"), $origInput, $ul);
  };
  $.havenInput["keyEvent" + KEY.LEFT] = $.havenInput["keyEvent" + KEY.UP] = function($li, $origInput, $ul){
    $.havenInput.focusPrevItem($li);
  };
  $.havenInput["keyEvent" + KEY.RETURN] = $.havenInput["keyEvent" + KEY.RIGHT] = $.havenInput["keyEvent" + KEY.DOWN] = function($li, $origInput, $ul){
    $.havenInput.focusNextItem($li);
  };

  $.fn.havenInput = function(options) {

    var otherArgs = Array.prototype.slice.call(arguments, 1);
    if (typeof options == 'string') {
      if (options === 'value' && (otherArgs.length === 0 || (otherArgs.length == 1 && typeof otherArgs[0] == 'string'))) {
        var values = []
        this.each(function(){
          var fn = $(this).data("havenInput.value");
          if (fn && $.isFunction(fn)) {
            values.push(fn.apply(this, otherArgs));
          }
        });
       if(values.length == 1) return values[0];
       else return values;
      }
      else {
        return this.each(function(){
          var fn = $(this).data("havenInput." + options);
          if (fn && $.isFunction(fn)) {
            fn.apply(this, otherArgs);
          }
        });
      }
    }

    var json = parseJson(options.source);
    if(json && !options.retrieveId) {
      options.source = json;
      options.retrieveId = options.retrieveValue = $.havenInput._defaultRetrieveFunc;
    }
    var opts = $.extend({}, $.fn.havenInput.defaults, options);
    // iterate and reformat each matched element
    return this.each(function(){
      $.havenInput._havenInput($(this), opts);
    });
  };

  //
  // plugin defaults
  //
  $.fn.havenInput.defaults = {
    /**
     * url/json for autocomplete
     */
    source : null,
    /**
     * key value pair for autocomplete request
     */
    data : {},
    /**
     * data to be added to the input when loaded. data need to be in the same format as the "source"
     */
    initData : [],
    /**
     * whether to cache the autocomplete data from server 
     */
    cache : false,
	/**
	 * whether to use autocomplete, if this is set to false or there is no $.autocomplete, autocomplete will be disabled. default to true
	 */
	useAC : true,
    /**
     * parameter name for the value to be send to the server
     */
    acParamName : "term",
    /**
     * maximum number of item allowed, 0 is unlimited
     */
    maxItem : 0,
    /**
     * Function for formatting autocomplete result in autocomplete's _renderItem
     */
    itemFormat : function(ul, item) {
      return this.retrieveValue(item);
    },
    /**
     * minimum length for firing autocomplete request
     */
    minLength : 2,
    /**
     * whether to hightlight the existing item if attempting to add duplicate 
     */
    highlightExist : true,
    /**
     * retrieve the value in a row/item of the data to be show, default to be the "name" attribute
     */
    retrieveValue : function(item) {return item.name}, 
    /**
     * retrieve the id value in a row/item of the data to be show, default to be the "id" attribute
     */
    retrieveId : function(item) {return item.id},
    /**
     * Function for cleaning input
     */
    doClean : emptyFunc,
    /**
     * if using ajax to retreive data, this will be called when data is recieved, put logic here if data needs to be manipulated. function(data) {}
     */
    onDataReceive : emptyFunc,
    /**
     * "focus" event call for autocomplete, with scope as the autocomplete input 
     */
    onAcFocus : function(e, ui) {this.val("")},
    /**
     * "select" event call for autocomplete, with scope as the autocomplete input 
     */
    onAcSelect : function(e, ui) {this.val("")},
    /**
     * "change" event call for autocomplete, with scope as the autocomplete input
     */
    onAcChange : function(e, ui) {this.val("")}  
  };
  
})(jQuery);